Часть III. Визуальное программирование в C++Builder
В этой заключительной, очень небольшой части книги мы обсудим некоторые аспекты “визуального программирования”, как обычно называют этот род деятельности. Другое название — Rapid Application Development (RAD), “ускоренная разработка приложений”. В отличие от описаний стандартного языка C/C++ в первых двух частях книги изложение будет носить совершенно неформальный характер. Мы будем ориентироваться в основном на разбор конкретных примеров. Я настоятельно рекомендую читателю побольше экспериментировать, изучать материалы оперативного справочника (кстати, сейчас существует, кажется. Help для C++Builder'a на русском языке) и вообще, так сказать, творчески подходить к делу. Визуальное программирование, при всей его кажущейся легкости, для рядового программиста оказывается своего рода искусством с неизбежным атрибутом в виде метода проб и ошибок, поскольку C++Builder и его библиотеки визуальных компонентов представляют собой очень большую и сложную систему.
Глава 14. Введение в визуальное программирование
Вы, конечно, уже знакомы с визуальным программированием в С+ + Builder, когда на экране перед вами находится фирма, на нее накладываются различные компоненты, реализующие элементы управления графического интерфейса, а затем пишется код для событий этих компонентов, использующий свойства последних. Возможно, вы работали раньше с Visual Basic или даже с Delphi. Все эти системы внешне очень похожи друг на друга, a Delphi — вообще двойник C++Builder'a и опирается на ту же самую библиотеку компонентов.
Простой пример
Начнем с очень простого примера. Создайте новое приложение (File New Application в главном меню или значок Application в диалоге New Items). На экране появится пустая форма. Уменьшите ее размер и, руководствуясь рис. 14.1, разместите на ней следующие компоненты: календарь (CCalendar) и кнопку прокрутки (CSpinButton) со страницы Samples палитры компонентов, а также командную кнопку и две метки со страницы Standard. (Я не буду приводить детальных пошаговых инструкций, поскольку вы и так наверняка умеете все это делать, а если не умеете, то полезно будет разобраться самому, да это и не сложно.)
Рис. 14.1 Форма программы в режиме проектирования
Установка свойств компонентов
Для дальнейшей работы вам потребуется инспектор объектов (если его нет на экране, откройте его через меню View). Чтобы проще было писать
код, назовите для краткости календарь Са1, а кнопку прокрутки — Spin (т. е. измените в инспекторе свойство Name этих компонентов).
Введите надписи (свойство Caption) для командной кнопки и второй метки (той. что справа, у меня она называлась Label2) — соответственно “Выход” и “Месяц”. Для первой метки (внизу) установите желаемый размер и, если хотите, гарнитуру шрифта, чтобы текст легко читался и выглядел прилично.
На этом этап визуального проектирования нашего приложения закончен. (Сейчас самое время сохранить проект, присвоив имена модулю формы, который по умолчанию называется Unitl.cpp, и главному модулю Ргоjectl.cpp, по имени которого будет назван весь проект и конечный исполняемый файл.) Теперь нужно написать код, который придаст существованию расположенных на форме компонентов какую-то осмысленность.
Этот простой проект содержит следующие основные файлы:
В проекте может быть несколько форм, и каждой из них будет соответствовать свой исходный модуль. Для каждой формы генерируется также заголовочный файл с расширением .h. Проект может включать в себя и модули исходного кода, не связанные непосредственно с какими-либо формами. Если вы создаете именно модуль (значок Unit в диалоге New Items), а не просто срр-файл, то C++Builder автоматически создаст и h-файл с тем же именем.
Ввод кода событий
Откройте страницу Events инспектора объектов. Общая методика написания кода такова: вы выбираете компонент (на форме или из выпадающего списка инспектора объектов) и дважды щелкаете кнопкой мыши на нужном событии в правой колонке страницы событий инспектора. C++Builder автоматически генерирует оболочку обработчика события и переключает фокус на окно редактора кода. Текстовый курсор стоит прямо там, куда вы должны ввести свой код.
Начните с кнопки “Выход”, которая должна закрывать форму, завершая тем самым приложение. Обработчик должен вызывать метод формы Close ().
Листинг 14.1. Файлы программы Prop — PropU.h и PropU.cpp
//---------------------------------------
// PropU.h: Заголовок для PropU.срр.
//---------------------------------------
#ifndef PropUH #define PropUH
//---------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include "CCALENDR.h"
#include <Grids.hpp>
#include "CSPIN.h"
//---------------------------------------
class TFormI : public TForm
{
_published: // IDE-managed Components
TButton *Buttonl;
TCCalendar *Cal;
TLabel *Labell;
TCSpinButton *Spin;
TLabel *Label2;
void _fastcall ButtonlClick(TObject *Sender);
void_fastcall CalChange(TObject *Sender);
void _fastcall SpinDownClick(TObject *Sender);
void _fastcall SpinUpClick(TObject *Sender) ;
void _fastcall FormCreate(TObject *Sender) ;
private: // User declarations
public: // User declarations
_fastcall TFormI(TComponent* Owner) ;
};
//---------------------------------------
extern PACKAGE TFormI *Forml;
//---------------------------------------
#endif
//---------------------------------------
// PropU.cpp: Исходный модуль примера с календарем.
//---------------------------------------
#include <vcl.h>
#pragma hdrstop #include <stdio.h>
#include "PropU.h"
//---------------------------------------
#pragma package(smart_init)
#pragma link "CCALENDR"
#pragma link "CSPIN"
#pragma resource "*.dfm"
TFormI *Forml;
//---------------------------------------
fastcall TFormI::TFormI(TComponent* Owner)
: TForm(Owner) {
}
//---------------------------------------
void_fastcall TFormI::ButtonlClick(TObject *Sender) {
Close () ;
}
//---------------------------------------
void_fastcall TFormI::CalChange(TObject *Sender)
{
char s[40] ;
sprintf(s, "Новая дата %d/%d/%d",
Cal->Day, Cal->Month, Cal->Year);
Labell->Caption = s;
}
//---------------------------------------
void_fastcall TFormI::SpinDownClick(TObject *Sender)
{
Cai->Day = 1;
if (Cal->Month != 1)
Cal->Month-;
else {
Cal->Month = 12;
Cal->Year-;
}
}
//---------------------------------------
void _fastcall TFormI::SpinUpClick(TObject *Sender) (
Cal->Day = 1;
if (Cal->Month != 12) Cal->Month++;
else {
Cal->Month = 1;
Cal->Year++;
}
//---------------------------------------
void _fastcall TFormI::FormCreate(TObject *Sender) {
char s[40] ;
sprintf(s, "Текущая дата %d/%d/%d",
Cal->Day, Cal->Month, Cal->Year);
Labell->Caption = s;
}
//---------------------------------------
В соответствии с листингом файла PropU.cpp введите код для тела обработчиков следующих событий:
Если вы случайно, щелкнув лишний раз мышью, создали ненужный обработчик какого-нибудь события, лучше не удалять его вручную (не забывайте также, что одновременно с оболочкой обработчика создается его объявление в классе формы в h-файле). При компиляции программы он будет удален автоматически; C++Builder сам удаляет все пустые обработчики событий.
На этом этап написания кода закончен. Все остальное, что вы видите в листинге, генерирует C++Builder, в том числе определение класса формы в файле PropU.h. Для компиляции и запуска приложения нажмите кнопку Run. Кнопка прокрутки позволяет менять текущий месяц (и год), который отображается календарем. В календаре можно выбрать мышью текущее число. Получившаяся дата отображается меткой Labell.
На рис. 14.2 показано окно программы сразу после запуска (вверху) и после некоторых манипуляций ее органами управления.
Рис.14.2 Запущенная программа
Как вы понимаете, ничего полезного эта программа не делает; она просто отображает (по событию OnChange календаря) выбранную пользователем дату. В настоящем приложении событие OnChange и извлеченная из компонента календаря дата могли бы, например, управлять чем-нибудь вроде ежедневника. Вся действительная работа с данными происходила бы “внутри” обработчика этого события в том смысле, что возврат из него означал бы переход программы в состояние ожидания выбора новой даты. Возможны, правда, и другие варианты организации потока управления, однако все равно — в приложении C++Builder любой программный код должен прямо или косвенно вызываться из некоторого обработчика события.
Класс формы
Определение класса формы создается в h-файле с тем же именем, что и имя модуля. Открыть в редакторе этот файл можно, выбрав в контекстном меню пункт Open Source/Header File. Как видите, класс содержит указатели на объекты размещенных на форме компонентов, а также объявления обработчиков событий. Все элементы класса формы (кроме открытого конструктора) объявлены в разделе с меткой _published (опубликованные). Объявление элемента класса в этом разделе эквивалентно объявлению в разделе public за исключением того, что опубликованные свойства и методы доступны в режиме проектирования через инспектор объектов. Например, поле выбранного события в правой колонке инспектора представляет собой комбинированный выпадающий список. При нажатии на стрелку отображаются доступные обработчики события с подходящим набором параметров. Если переместить, например, обработчик ButtonlClick из раздела _published в раздел public, в этих списках вы его больше не увидите, однако вы можете ввести его имя вручную или, скажем, установить обработчик программно.
Как вы, без сомнения, также заметили, все функции-элементы класса формы объявлены со спецификацией _fastcall. Этот спецификатор протокола вызова описывался в 4-й главе. Он означает, что аргументы при вызове такой функции должны по возможности передаваться в регистрах процессора.
В библиотеке VCL применяется исключительно соглашение о вызове _fastcall. Функции, написанные вами, не обязаны иметь эту спецификацию, однако если функция является методом формы или компонента, разумным будет объявить ее именно так. Во-первых, этот вызов действительно быстрее вызова С или stdcall, а во-вторых, просто ради единообразия.
Компоненты, свойства и события
Основным строительным элементом визуального программирования является компонент. В свою очередь, для компонентов, в отличие от обыкновенных объектов C++, характерно наличие свойств и событий.
Компоненты
Под компонентами в C++Builder понимаются объекты или классы объектов, являющиеся, в некотором смысле, объектами “реального мира”. Вы непосредственно видите их на экране, их можно передвигать мышью, они реагируют на нажатие ее кнопок и т. д. Компоненты VCL инкапсулированы в классах языка Object Pascal, однако вполне возможно и написание компонентов на расширенном варианте C++, реализуемом в C++Builder.
Компоненты календаря и кнопки прокрутки, которые мы взяли для вышеприведенного примера, включены в палитру компонентов именно в качестве образцов такого рода. На самом деле стандартный календарь из VCL со страницы Win32 уже имеет все то, что мы реализовали в своем примере.
Чтобы можно было подключать библиотеку VCL к программам на C++, она сопровождается заголовочными файлами C++, моделирующими разделы интерфейса модулей языка Pascal. Эти заголовки содержат “параллельные” определения классов VCL. Заголовки VCL имеют по большей части расширение .hpp.
VCL расшифровывается как “библиотека визуальных компонентов”. Однако среди ее компонентов попадаются и не визуальные. Не визуальным компонентом является, например, таймер (класс TTimer). Компоненты стандартных диалогов тоже, как это ни странно, не визуальны.
Поля и методы
Поле — это просто другое название для элемента данных класса. Соответственно метод — синоним для функции-элемента класса. Для классов компонентов в C++Builder применяются именно эти термины.
Свойства
На первый взгляд свойство компонента не отличается от обычного элемента данных класса (поля). Действительно, в приведенном выше листинге мы видим выражения вроде
Cal->Month =12;
Cal->Year++;
Свойству можно присваивать значение, извлекать из него значение и вообще вроде бы делать с ним все то, что делают с простым полем. Действительно, свойство, как правило, имеет ассоциированное с ним поле, и иногда операции над свойством не означают ничего, кроме соответствующих операций над его полем. Но в примере из предыдущего раздела вы могли видеть, как простое присваивание значения, например, свойству Month календаря сразу меняет все его содержимое. Другими словами, свойство может иметь непосредственный коррелят в “реальном мире” компьютерного экрана. Механизм свойств обеспечивает компонентам ту их реальность, о которой мы говорили чуть выше.
Все это означает, что изменение значения свойства должно сопровождаться некоторыми побочными действиями, и потому присваивание значения свойству реализуется посредством вызова некоторой функции. Такие функции называют обычно set-функциями, а функции для чтения свойств — get-функциями. И те и другие называются функциями доступа.
Наш пример с календарем позволит нам исследовать некоторые аспекты свойств. Откройте окно обозревателя классов, если оно еще не открыто (View Explorer в контекстном меню редактора). Найдите в нем узел TCCalendar и дважды щелкните на нем кнопкой мыши. В редакторе откроется файл ccalendr.h, и курсор будет установлен на начало определения класса календаря. В разделе published вы можете видеть несколько типичных объявлений свойств, например:
_property TDayOfWeek StartOfWeek =(read=FStartOfHeek, write=SetStartOfWeek, defauit=l);
В разделе private объявлено ассоциированное поле:
TDayOfWeek FStartOfWeek;
TDayOfWeek — это просто short. В фигурных скобках записан список атрибутов свойства, который означает, что:
Set-функция, как и поле свойства, объявлена в разделе private:
void _fastcall SetStartOfWeek(TDayOfWeek Value);
Объявление свойства Month несколько сложнее:
property Integer Month = {read=GetDateElement,
write=SetDateElement,
stored=false,
index=2,
nodefault};
Атрибуты stored и nodefault относятся к так называемым спецификаторам хранения. Атрибут index показывает, что функции доступа должны вызываться с дополнительным (первым) аргументом, равным 2. Вот объявление set-функции:
void _fastcall SetDateElement(int Index, int Value);
На самом деле календарь сохраняет дату в единственном поле FDate типа TDateTime. Свойства Year, Month, Day не имеют собственных полей, а их функции доступа (они одни и те же, только с разными индексами) оперируют полем FDate.
Исходный модуль с кодом календаря ccalendr.cpp вы можете найти в папке ...\CBuilder5\Examples\Controls\Source.
События
С точки зрения определения класса компонента события являются просто свойствами определенного типа. В том же классе календаря:
private:
TNotifyEvent FOnChange;
published:
property TNotifyEvent OnChange = {read=FOnChange,
write=FOnChange};
Тип свойства-события должен быть так называемым замыканием (closure), о котором мы подробнее расскажем в следующей главе. Пока имейте в виду, что это специального вида указатель на функцию-элемент.
TNotifyEvent — простейший тип события, не имеющий дополнительных параметров кроме указателя па пославший событие объект:
typedef void _fastcall
(closure *TNotifyEvent)(System::TObject* Sender);
События, как и Другие свойства, можно читать и записывать. Инспектор объектов, например, позволяет присноить событию компонента указатель на требуемую процедуру обслуживания.
Особенность событий состоит в том, что они связывают компонент с внешним миром, позволяя компоненту играть активную роль в общении с ним.
При всяком изменении состояния календарь вызывает свой метод Change ():
void _fastcall TCCalendar::Change() {
if(FOnChange)
FOnChange(this);
}
Метод, в свою очередь, вызывает процедуру, указатель на которую записан в поле свойства OnChange.
Еще один пример
В заключение главы мы покажем еще один пример, который не продемонстрирует ничего особенно нового, но послужит основой для разработки нашего собственного компонента в следующей главе. Пример выводит на форму “бегущую строку”.
Форма
Проектирование формы сводится к установке подходящего (небольшого) ее размера и размещению всего трех командных кнопок с надписями “Старт”, “Стоп” и “Выход”. Вы можете руководствоваться рис. 14.5, на котором показана запущенная программа.
Код
После этого нужно ввести код программы. Помимо кода обработчиков OnClick для кнопок и OnPaint для формы, вам потребуется написать:
Код программы показывает следующий листинг.
Листинг 14.2. Файлы RunningU.h и RunningU.cpp
//---------------------------------------
// RunningU.h
//---------------------------------------
#ifndefRunningUII
#define RunninqUH
//---------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <:Forms.hpp>
#include <ExtCtrls.hpp>
//---------------------------------------
class TFormI : public TForm {
_published: // IDE-managed Components
TButton *Buttonl;
TButton *Button2;
TButton *Button3;
void _fastcall Button3Click(TObject *Sender);
void _fastcail Button1Click(TObject *Sender);
void _fastcall Button2Click(TObject *Sender);
void _fastcall FormPaint(TObject *Senders-private:
// User declarations
Graphics::TBitmap *bm;
inL position;
bool started;
void fastcall Setup();
void _fastcall Loop();
public: // User declarations
int interval;
_fastcall TFormI(TComponent* Owner);
_fastcail ~TForml<);
};
//---------------------------------------
extern PACKAGE TFormI *Forml;
//---------------------------------------
#endif
//---------------------------------------
// RunningU.cpp: Исходный модуль программы
//с "бегущей строкой".
//---------------------------------------
#include <vcl.h>
#pragma hdrstop
#include <sys\timeb-:h>
#include "RunningU.h"
//---------------------------------------
#pragma package (smart_init)
#pragma resource "*.dfm"
TFormI *Forml;
char tent[80] = "Тестирование довольно длинной бегущей
строки...";
//---------------------------------------
// Конструктор формы - выделяет и инициализирует
// битовую матрицу.
//
_fast-call TFormI: :TForml (TComponent* Owner)
: TForm(Owner) {
position = Width;
interval = 10;
started = false;
bm = new Graphics::TBitmap;
Setup 0;
}
//---------------------------------------
// Деструктор формы - удаляет битовую матрицу.
//
_fastcall TFormI::--TFormI() (
delete bm;
}
//---------------------------------------
//Инициализирует битовую матрицу образом текстовой строки.
void _fastcall TFormI:: Setup ()
bm->Canvas->Font->Name = "Comic.Sans MS";
bm->Canvas->Font->Size = 16;
bm->Height = bm->Canvas->TextHeight(text);
bm->Width = bm->Canvas->TextWidth(text) + 1;
bm->Canvas->Brush->Color = clBtnFace;
bm->Canvas->FillRect(Rect(0, 0, bm->Width, bm->Height)) ;
bm->Canvas->TextOut (0, 0, text) ;
}
//---------------------------------------
// цикл бегущей строки. Организует таймер с помощью
//Функциии API GetTickCount () .
//
void_fastcall TFormI::Loop() {
unsigned long nextTick = GetTickCount();
while (started) {
Application->ProcessMes sages ();
if (!started) return; if (GetTickCount () > nextTick)
{
//
// Копировать битовую матрицу с текстом
// на канву формы.
//
Canvas->Draw(--position, 12, bm);
if (position < -bm->Width) position = Width;
nextTick += interval;
} }
//---------------------------------------
// Кнопка "Выход".
//
void fastcall TFornil::Button3Click(TObject *Sender)
{ —
started = false;
Close () ;
}
//---------------------------------------
// Кнопка "Старт".
//
void_fastcall TFormI::ButtonlClick(TObject *Sender)
{
if (started) return;
started = true;
Loop() ;
} //-------------------------------------
// Кнопка "Стоп".
//
void _fastcall TFormI::Button2Click(TObject *Sender)
{
started = false;
}
//---------------------------------------
// Событие OnPaint.
// Обеспечивает обновление изображения
// при остановленной строке.
//
void _fastcall TFormI::FormPaint(TObject *Sender)
{
if (!started)
Canvas->Draw(position, 12, bm) ;
}
Для определения элементов любого класса можно воспользоваться услугами ClassExplorer. Щелкните правой кнопкой на узле нужного класса. Контекстное меню имеет пункты New Field..., New Property... и New Method... Эти пункты меню вызывают соответствующие диалоги, автоматически генерирующие необходимый код, по крайней мере его основу. Например, New Method вводит в класс объявление метода и создает в исходном модуле его оболочку.
Обратите внимание на методы Setup () и Loop (). В первом из них используется свойство Canvas битовой матрицы, созданной в конструкторе формы, во втором — свойство Canvas формы. Это свойство имеется у очень многих компонентов и инкапсулирует контекст графического устройства
Windows (DC). Подсвойства Canvas (такие, как Font, Brush и т. гг.) представляют различные графические объекты. Методы класса TCanvas позволяют рисовать основные графические формы, выводить текст, копировать изображения с одной канвы на другую и выполнять другие операции, связан Метод Loop (), вызываемый при нажатии кнопки “Старт”, содержит цикл ожидания, из которого программа не выходит до тех пор, пока не будет нажата кнопка “Стоп”. На каждом проходе цикла вызывается функция ProcessMessages () объекта Application. Эта функция передает управление системе Windows, чтобы последняя могла обработать находящиеся в очереди сообщения.
Метод использует также функцию API GetTickCount (), которая возвращает число миллисекунд, прошедшее с момента запуска системы. Цикл ожидания проверяет, достигло ли это число маркера времени, хранящегося в переменной nextTick, и если достигло, выводит текст на экран в следующей позиции, сдвигая при этом маркер времени дальше на заданный интервал.
На рисунке показана работающая программа.
Рис. 14.3 Программа бегущей строки
В библиотеке VCL имеется таймер (класс TTimer), который может периодически вырабатывать событие OnTimer. Применение в этой программе таймера позволило бы не прибегать к методике такого, не очень красивого, цикла ожидания. Беда в том, что компонент таймера не может обеспечить достаточно короткий интервал генерирования события. Хотя интервал таймера задается в единицах миллисекунд, на самом деле он оказывается кратным 55 ms, т. е. частота срабатывания таймера не может быть выше 18 герц. (По крайней мере, так обстоит дело в Windows 98, с которой я работаю.) Частота это определяется аппаратным таймером системы. Нам же нужна частота порядка сотни герц, чтобы получилась приемлемая скорость сдвига строки.
Заключение
В этой главе мы представили довольно элементарный материал по основам визуального программирования в C++Builder. Мы немного рассказали о концепции свойств, являющейся, по сути, центральным моментом всего визуального программирования. Далее мы подробнее исследуем устройство визуальных компонентов на примере собственноручно написанного специального компонента.